﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace BitmapTagger
{

    /// <summary>
    /// Represents the tags of a jpeg
    /// </summary>
    public class JpegTagger : BitmapTagger
    {


        /// <summary>
        /// The base of the query string used for exif queries
        /// </summary>
        private const string exifQueryBase = "/app1/ifd/exif/";

        /// <summary>
        /// The base of the query string used for ifd queries
        /// </summary>
        private const string ifdQueryBase = "/app1/ifd/";

        /// <summary>
        /// The base of the query string used for xmp queries
        /// </summary>
        private const string xmpQueryBase = "/xmp/";

        /// <summary>
        /// The end of the query string used for padding.
        /// </summary>
        private const string paddingSchemeQueryEnd = "PaddingSchema:Padding";


        /// <summary>
        /// Create a new jpeg tagger
        /// </summary>
        /// <param name="filePath">The file path of the jpeg image</param>
        /// <param name="loadExif">Whether or not to load Exif tags</param>
        /// <param name="loadXMPExif">Whether or not to load XMPExif tags</param>
        /// <param name="loadXMP">Whether or not to load XMP tags</param>
        /// <param name="precedence">The precedence of the tagging systems</param>
        public JpegTagger(string filePath, bool loadExif, bool loadXMPExif, bool loadXMP,
            TagPrecedence precedence)
        {
            Precedence = precedence;
            Load(filePath, loadExif, loadXMPExif, loadXMP, Precedence);
        }

        /// <summary>
        /// Create a new JpegTagger
        /// </summary>
        /// <remarks>
        /// Defaults to load all tags
        /// Defaults Tagging System Precendence as Exif > XMPExif > XMP
        /// </remarks>
        /// <param name="filePath">The file path of the jpeg image</param>
        public JpegTagger(string filePath) : this(filePath, true, true, true, TagPrecedence.Exif_XMPExif_XMP) { }


        #region save


        /// <summary>
        /// Save the JPEG to the same file path it was loaded from
        /// </summary>
        /// <param name="saveExif">Whether or not to save the information to Exif tags</param>
        /// <param name="saveXMPExif">Whether or not to save the information to XMPExif tags</param>
        /// <param name="saveXMP">Whether or not to save the information to XMP tags</param>
        public override void Save(bool saveExif, bool saveXMPExif, bool saveXMP)
        {
            string tempFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + System.IO.Path.DirectorySeparatorChar + "temp.jpg";
            using (Stream inputFile = File.Open(FilePath, FileMode.Open, FileAccess.ReadWrite))
            {
                BitmapDecoder original = BitmapDecoder.Create(inputFile, BitmapCreateOptions.None, BitmapCacheOption.Default);
                BitmapMetadata metadata = original.Frames[0].Metadata.Clone() as BitmapMetadata;


                metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", PaddingAmount);
                metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", PaddingAmount);
                metadata.SetQuery("/xmp/PaddingSchema:Padding", PaddingAmount);


                if (saveExif)
                    SaveExif(metadata);

                if (saveXMPExif)
                    SaveXMPExif(metadata);

                if (saveXMP)
                    SaveXMP(metadata);

                //Recreate the image file with the new metadata
                JpegBitmapEncoder output = new JpegBitmapEncoder();
                output.Frames.Add(BitmapFrame.Create(original.Frames[0], original.Frames[0].Thumbnail, metadata, original.Frames[0].ColorContexts));
                //Must save to a temporary location as the stream to the original file is still open
                using (Stream outputFile = File.Create(tempFile))
                {
                    output.Save(outputFile);
                    outputFile.Close();
                }

                inputFile.Close();
            }

            //Move the temp file to be in place of the current file
            File.Copy(tempFile, FilePath, true);
            File.Delete(tempFile);

        }


        /// <summary>
        /// Save the JPEG 
        /// </summary>
        /// <param name="filePath">The file path to save to</param>
        /// <param name="saveExif">Whether or not to save the information to Exif tags</param>
        /// <param name="saveXMPExif">Whether or not to save the information to XMPExif tags</param>
        /// <param name="saveXMP">Whether or not to save the information to XMP tags</param>
        public override void Save(string filePath, bool saveExif, bool saveXMPExif, bool saveXMP)
        {
            if (!filePath.Equals(FilePath))
            {
                File.Copy(FilePath, filePath, true);
                FilePath = filePath;
            }

            Save(saveExif, saveXMPExif, saveXMP);
        }

        /// <summary>
        /// Save the JPEG with the tags to the location it was loaded from
        /// </summary>
        /// <remarks>
        /// When using Save() the thread it is called in must be marked at as a Single Thread Apartment (STA)
        /// Defaults as saving to all tagging systems (Exif, XMPExif, and XMP)
        /// </remarks>
        public override void Save()
        {
            Save(true, true, true);
        }


        /// <summary>
        /// Save the image to the specified file path, after the save
        /// the specified file path will be considered the file path of the 
        /// image it is working from`
        /// </summary>
        /// <remarks>
        /// When using Save the thread it is called in must be marked at as a Single Thread Apartment (STA)
        /// Defaults as saving to all tagging systems (Exif, XMPExif, and XMP)
        /// </remarks>
        /// <param name="filePath">The file path to have the image saved to</param>
        public override void Save(string filePath)
        {
            File.Copy(FilePath, filePath, true);
            FilePath = filePath;
            Save();
        }


        /// <summary>
        /// Save to the Exif tags
        /// </summary>
        /// <remarks>
        /// This will save each property to its corresponding Exif tag if an Exif tag is available for 
        /// the property
        /// </remarks>
        /// <param name="metadata">The metadata to save the tags to</param>
        private void SaveExif(BitmapMetadata metadata)
        {
            //YResolution
            if (YResolution != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.YResolutionID), YResolution.ToUInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.YResolutionID));

            //XResolution
            if (XResolution != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.XResolutionID), XResolution.ToUInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.XResolutionID));

            //DateTime
            if (DateTime != null)
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID), GetExifTagStringFromDateTime(DateTime));
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID));

            //DateTime Original
            if (DateTimeOriginal != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID), GetExifTagStringFromDateTime(DateTimeOriginal));
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID));

            //DateTime Digitized
            if (DateTimeDigitized != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID), GetExifTagStringFromDateTime(DateTimeDigitized));
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID));

            //Image Description
            if (!String.IsNullOrEmpty(ImageDescription))
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID), ImageDescription);
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID));


            //Make
            if (!String.IsNullOrEmpty(Make))
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.MakeID), Make);
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.MakeID));


            //Model
            if (!String.IsNullOrEmpty(Model))
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.ModelID), Model);
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.ModelID));

            //Software
            if (!String.IsNullOrEmpty(Software))
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID), Software);
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID));

            //Artist
            if (!String.IsNullOrEmpty(Artist))
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID), Artist);
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID));

            //Copyright
            if (!String.IsNullOrEmpty(Copyright))
                metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID), Copyright);
            else
                metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID));

            //UserComment
            if (!String.IsNullOrEmpty(UserComment))
            {
                BitmapMetadataBlob blob = new BitmapMetadataBlob(System.Text.ASCIIEncoding.ASCII.GetBytes(UserComment));
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID), blob);
            }
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID));

            //Metering Mode 
            if (MeteringMode != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID), (ushort)MeteringMode);
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID));

            //Maker Note
            if (!String.IsNullOrEmpty(MakerNote))
            {
                BitmapMetadataBlob blob = new BitmapMetadataBlob(System.Text.ASCIIEncoding.ASCII.GetBytes(MakerNote));
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID), blob);
            }
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID));

            //Exposure Program
            if (ExposureProgram != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID), ExposureProgram);
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID));

            //Exposure Time
            if (ExposureTime != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ExposureTimeID), ExposureTime.ToUInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID));


            //ISO Speed Ratings
            if (ISOSpeedRatings != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID), ISOSpeedRatings);
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID));

            //Light Source
            if (LightSource != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID), LightSource);
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID));

            //FNumber
            if (FNumber != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.FNumberID), FNumber.ToUInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.FNumberID));

            //Shutter Speed
            if (ShutterSpeed != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID), ShutterSpeed.ToInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID));

            //Brightness
            if (Brightness != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.BrightnessID), Brightness.ToInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.BrightnessID));

            //Aperture
            if (Aperture != null)
                metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ApertureID), Aperture.ToUInt64());
            else
                metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ApertureID));
        }

        /// <summary>
        /// Save to the XMPExif tags
        /// </summary>
        /// <remarks>
        /// This will save each property to its corresponding XMPExif tag if an XMPExif tag is available for 
        /// the property
        /// </remarks>
        /// <param name="metadata">The metadata to save the tags to</param>
        private void SaveXMPExif(BitmapMetadata metadata)
        {
            //YResolution
            if (YResolution != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.YResolutionID, YResolution.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.YResolutionID);

            //XResolution
            if (XResolution != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.XResolutionID, XResolution.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.XResolutionID);

            //DateTime
            if (DateTime != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.DateTimeID, GetXMPTagStringFromDateTime(DateTime));
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DateTimeID);

            //DateTime Original
            if (DateTimeOriginal != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID, GetXMPTagStringFromDateTime(DateTimeOriginal));
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID);


            //DateTime Digitized
            if (DateTimeDigitized != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID, GetXMPTagStringFromDateTime(DateTimeDigitized));
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID);

            //Image Description
            if (!String.IsNullOrEmpty(ImageDescription))
            {
                SetPrimaryValueForLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.ImageDescriptionID, ImageDescription);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ImageDescriptionID);

            //Make
            if (!String.IsNullOrEmpty(Make))
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.MakeID, Make);
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.MakeID);

            //Model
            if (!String.IsNullOrEmpty(Model))
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ModelID, Model);
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ModelID);

            //Software
            if (!String.IsNullOrEmpty(Software))
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.SoftwareID, Software);
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.SoftwareID);

            //Artist
            if (!String.IsNullOrEmpty(Artist))
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ArtistID, Artist);
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ArtistID);

            //Copyright
            if (!String.IsNullOrEmpty(Copyright))
            {
                SetPrimaryValueForLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.CopyrightID,
                    Copyright);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.CopyrightID);

            //UserComment
            if (!String.IsNullOrEmpty(UserComment))
            {
                SetPrimaryValueForLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.UserCommentID, UserComment);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.UserCommentID);


            //Metering Mode 
            if (MeteringMode != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID, MeteringMode.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID);

            //Exposure Program
            if (ExposureProgram != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID, ExposureProgram.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID);


            //Exposure Time
            if (ExposureTime != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID, ExposureTime.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID);


            //ISO Speed Ratings
            if (ISOSpeedRatings != null)
            {
                SetPrimaryValueForXMPSequence(metadata,
                    xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID,
                    ISOSpeedRatings);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID);

            //Light Source
            if (LightSource != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.LightSourceID, LightSource.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.LightSourceID);

            //FNumber
            if (FNumber != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.FNumberID, FNumber.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.FNumberID);

            //Shutter Speed
            if (ShutterSpeed != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID, ShutterSpeed.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID);

            //Brightness
            if (Brightness != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.BrightnessID, Brightness.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.BrightnessID);

            //Aperture
            if (Aperture != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ApertureID, Aperture.ToString());
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ApertureID);
        }


        /// <summary>
        /// Save to the XMP tags
        /// </summary>
        /// <remarks>
        /// This will save each property to its corresponding XMP tag if an XMP tag is available for 
        /// the property
        /// </remarks>
        /// <param name="metadata">The metadata to save the tags to</param>
        private void SaveXMP(BitmapMetadata metadata)
        {

            //DateTime
            if (DateTime != null)
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID, GetXMPTagStringFromDateTime(DateTime));
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID);

            //Image Description
            if (!String.IsNullOrEmpty(ImageDescription))
            {
                SetPrimaryValueForLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.DescriptionID,
                    ImageDescription);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DescriptionID);

            //Software
            if (!String.IsNullOrEmpty(Software))
                metadata.SetQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID, Software);
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID);

            //Artist
            if (!String.IsNullOrEmpty(Artist))
            {
                SetPrimaryValueForXMPSequence(metadata, xmpQueryBase + JpegXmpIDs.CreatorID, Artist);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.CreatorID);

            //Copyright
            if (!String.IsNullOrEmpty(Copyright))
            {
                SetPrimaryValueForLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.RightsID,
                    Copyright);
            }
            else
                metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.RightsID);
        }

        #endregion

        #region load


        /// <summary>
        /// Load a JPEG
        /// </summary>
        /// <param name="filePath">The file path of the JPEG to load</param>
        /// <param name="loadExif">Whether or not to load Exif tags</param>
        /// <param name="loadXMPExif">Whether or not to load XMPExif tags</param>
        /// <param name="loadXMP">Whether or not to load XMP tags</param>
        /// <param name="precendenceOfTags">The precendece of the tagging systems (used for conflicts)</param>
        public override void Load(string filePath, bool loadExif, bool loadXMPExif, bool loadXMP,
            TagPrecedence precendenceOfTags)
        {
            Precedence = precendenceOfTags;
            Debug.Assert(ExtensionChecker.IsJPEGExtension(System.IO.Path.GetExtension(filePath)),
                "JpegTagger can only load a jpeg file");
            FilePath = filePath;


            using (Stream originalFile = File.Open(FilePath, FileMode.Open, FileAccess.Read))
            {
                try
                {
                    BitmapDecoder originalImage = BitmapDecoder.Create(originalFile, BitmapOptions, BitmapCacheOption.None);
                    var metadata = originalImage.Frames[0].Metadata.Clone() as BitmapMetadata;

                    metadata.SetQuery(ifdQueryBase + paddingSchemeQueryEnd, PaddingAmount);
                    metadata.SetQuery(exifQueryBase + paddingSchemeQueryEnd, PaddingAmount);
                    metadata.SetQuery(xmpQueryBase + paddingSchemeQueryEnd, PaddingAmount);

                    //Image Width  -- In JPEG this is held in a JPEG marker so we are obtaining it from
                    //the JPEG decoder
                    ImageWidth = (uint)originalImage.Frames[0].PixelWidth;

                    //Image Height  --In JPEG this is held in a JPEG marker so we are obtaining it from
                    //the JPEG decoder
                    ImageHeight = (uint)originalImage.Frames[0].PixelHeight;

                    if (precendenceOfTags == TagPrecedence.Exif_XMP_XMPExif)
                    {
                        if (loadXMPExif)
                            LoadXMPExif(metadata);

                        if (loadXMP)
                            LoadXMP(metadata);

                        if (loadExif)
                            LoadExif(metadata);
                    }
                    else if (precendenceOfTags == TagPrecedence.Exif_XMPExif_XMP)
                    {
                        if (loadXMP)
                            LoadXMP(metadata);

                        if (loadXMPExif)
                            LoadXMPExif(metadata);

                        if (loadExif)
                            LoadExif(metadata);
                    }
                    else if (precendenceOfTags == TagPrecedence.XMP_Exif_XMPExif)
                    {
                        if (loadXMPExif)
                            LoadXMPExif(metadata);

                        if (loadExif)
                            LoadExif(metadata);

                        if (loadXMP)
                            LoadXMP(metadata);
                    }
                    else if (precendenceOfTags == TagPrecedence.XMP_XMPExif_Exif)
                    {
                        if (loadExif)
                            LoadExif(metadata);

                        if (loadXMPExif)
                            LoadXMPExif(metadata);

                        if (loadXMP)
                            LoadXMP(metadata);
                    }
                    else if (precendenceOfTags == TagPrecedence.XMPExif_Exif_XMP)
                    {
                        if (loadXMP)
                            LoadXMP(metadata);

                        if (loadExif)
                            LoadExif(metadata);

                        if (loadXMPExif)
                            LoadXMPExif(metadata);
                    }
                    else if (precendenceOfTags == TagPrecedence.XMPExif_XMP_Exif)
                    {
                        if (loadExif)
                            LoadExif(metadata);

                        if (loadXMP)
                            LoadXMP(metadata);

                        if (loadXMPExif)
                            LoadXMPExif(metadata);
                    }
                    else
                    {
                        throw new Exception("Unknown TagPrecedence");
                    }
                }
                catch { }
                originalFile.Close();
            }
        }

        /// <summary>
        /// Load a JPEG
        /// </summary>
        /// <remarks>
        /// Defaults all tags to be loaded with a precedence of Exif > XMPExif > XMP
        /// </remarks>
        /// <param name="filePath">The file path of the JPEG to load</param>
        public override void Load(string filePath)
        {
            Debug.Assert(System.IO.Path.GetExtension(filePath).Equals(".jpg"),
                "JpegTagger can only load a jpeg file");
            FilePath = filePath;

            Load(filePath, true, true, true, TagPrecedence.Exif_XMPExif_XMP);
        }

        /// <summary>
        /// Load the Exif tags
        /// </summary>
        /// <remarks>
        /// When loading a tag it will overwrite any previous loaded tag information
        /// <example>
        /// If the ExifXMP tag for Aperture has already been loaded and then the Exif tag for
        /// Aperture is loaded in this method, the Aperture will be assigned the value of the Exif tag
        /// </example>
        /// </remarks>
        /// <param name="metadata">The metadata the tags are contained in</param>
        private void LoadExif(BitmapMetadata metadata)
        {

            //XResolution
            if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.XResolutionID)))
                try
                {
                    XResolution = new URational((UInt64)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.XResolutionID)));
                }
                catch { }

            //YResolution
            if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.YResolutionID)))
                try
                {
                    YResolution = new URational((UInt64)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.YResolutionID)));
                }
                catch { }

            //Resolution Unit
            if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ResolutionUnitID)))
                try
                {
                    ResolutionUnit = (ushort?)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ResolutionUnitID));
                }
                catch { }
            //DateTime
            if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID)))
                try
                {
                    DateTime = GetDateTimeFromExifString((string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID)));
                }
                catch { }

            //Image Description
            if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID)))
                ImageDescription = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID));


            //Make
            if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.MakeID)))
            {
                try
                {
                    Make = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.MakeID));
                }
                catch { }

                //Model
                try
                {
                    if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ModelID)))
                    {
                        Model = GetStringFromPossibleSequence(metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ModelID)));
                    }
                }
                catch { }

                try
                {
                    //Software
                    if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID)))
                        Software = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID));
                }
                catch { }

                try
                {
                    //Artist
                    if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID)))
                        Artist = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID));
                }
                catch { }


                try
                {
                    //Copyright
                    if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID)))
                        Copyright = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID));
                }
                catch { }

                //UserComment
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID)))
                {
                    if (metadata is BitmapMetadata)
                    {
                        UserComment = string.Empty;
                    }
                    else
                    {
                        BitmapMetadataBlob blob = (BitmapMetadataBlob)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID));
                        UserComment = System.Text.Encoding.ASCII.GetString(blob.GetBlobValue());
                    }
                }


                //DataTimeOriginal
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID)))
                {
                    try
                    {
                        DateTimeOriginal = GetDateTimeFromExifString((string)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID)));
                    }
                    catch { }
                }


                //DataTimeDigitized
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID)))
                    try
                    {
                        DateTimeDigitized = GetDateTimeFromExifString((string)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID)));
                    }
                    catch { }

                //Metering Mode
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID)))
                    MeteringMode = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID));

                //ManufactureNote
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID)))
                {
                    try
                    {
                        BitmapMetadataBlob blob = (BitmapMetadataBlob)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID));
                        MakerNote = System.Text.Encoding.Default.GetString(blob.GetBlobValue());
                    }
                    catch { }
                }

                //Sensing Method
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.SensingMethodID)))
                    SensingMethod = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.SensingMethodID));

                //ExposureProgram
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID)))
                    ExposureProgram = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID));

                //ExposureTime
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ExposureTimeID)))
                    ExposureTime = new URational((UInt64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ExposureTimeID)));


                //ISO Speed Ratings
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID)))
                    ISOSpeedRatings = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID));

                //Light Source
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID)))
                    LightSource = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID));


                //FNumber
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.FNumberID)))
                    FNumber = new URational((UInt64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.FNumberID)));

                //Shutter Speed
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID)))
                    ShutterSpeed = new Rational((Int64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID)));

                //Brightness
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.BrightnessID)))
                    Brightness = new Rational((Int64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID)));

                //Aperture
                if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ApertureID)))
                    Aperture = new URational((UInt64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ApertureID)));
            }
        }


        /// <summary>
        /// Load the XMP tags
        /// </summary>
        /// <remarks>
        /// When loading a tag it will overwrite any previous loaded tag information
        /// <example>
        /// If the ExifXMP tag for ImageDescription has already been loaded and then the XMP tag for
        /// Description is loaded in this method, the Descirption will be assigned the value of the XMP tag
        /// </example>
        /// </remarks>
        /// <param name="metadata">The metadata the tags are contained in</param>
        private void LoadXMP(BitmapMetadata metadata)
        {

            //DateTime
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID))
                DateTime = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID));

            //Image Description
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DescriptionID))
            {
                ImageDescription =
                    (string)GetPrimaryValueFromLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.DescriptionID);
            }


            //Software
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID))
                Software = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID);

            //Artist
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.CreatorID))
            {
                Artist = (string)GetPrimaryValueFromXMPSequence(metadata,
                    xmpQueryBase + JpegXmpIDs.CreatorID);
            }

            //Copyright
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.RightsID))
            {
                Copyright =
                    (string)GetPrimaryValueFromLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.RightsID);
            }
        }


        /// <summary>
        /// Load the XMPExif tags
        /// </summary>
        /// <remarks>
        /// When loading a tag it will overwrite any previous loaded tag information
        /// <example>
        /// If the Exif tag for Aperture has already been loaded and then the XMPExif tag for
        /// Aperture is loaded in this method, the Aperture will be assigned the value of the XMPExif tag
        /// </example>
        /// </remarks>
        /// <param name="metadata">The metadata the tags are contained in</param>
        private void LoadXMPExif(BitmapMetadata metadata)
        {

            //XResolution
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.XResolutionID))
                XResolution = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.XResolutionID));


            //YResolution
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.YResolutionID))
                YResolution = new URational(((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.YResolutionID)));


            //Resolution Unit
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ResolutionUnitID))
                ResolutionUnit = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ResolutionUnitID));

            //DateTime
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DateTimeID))
                DateTime = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.DateTimeID));

            //Image Description
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ImageDescriptionID))
            {
                ImageDescription =
                    (string)GetPrimaryValueFromLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.ImageDescriptionID);
            }


            //Make
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.MakeID))
                Make = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.MakeID);

            //Model
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ModelID))
                Model = GetStringFromPossibleSequence((metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ModelID)));


            //Software
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.SoftwareID))
                Software = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.SoftwareID);


            //Artist
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ArtistID))
                Artist = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ArtistID);


            //Copyright
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.CopyrightID))
            {
                Copyright = (string)GetPrimaryValueFromLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.CopyrightID);
            }


            //UserComment
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.UserCommentID))
            {
                UserComment =
                    (string)GetPrimaryValueFromLangAlt(metadata,
                    xmpQueryBase + JpegXmpIDs.UserCommentID);
            }


            //DataTimeOriginal
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID))
                DateTimeOriginal = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID));


            //DataTimeDigitized
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID))
                DateTimeDigitized = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID));

            //Metering Mode
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID))
                MeteringMode = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID));


            //Sensing Method
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.SensingMethodID))
                SensingMethod = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.SensingMethodID));

            //ExposureProgram
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID))
                ExposureProgram = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID));

            //ExposureTime
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID))
                ExposureTime = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID));


            //ISO Speed Ratings
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID))
            {
                object temp = GetPrimaryValueFromXMPSequence(metadata,
                    xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID);

                if (temp != null)
                    ISOSpeedRatings = Convert.ToUInt16(temp);
            }

            //Light Source
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.LightSourceID))
                LightSource = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.LightSourceID));


            //FNumber
            if (metadata.Contains(xmpQueryBase + JpegXmpIDs.FNumberID))
                FNumber = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.FNumberID));


            //Shutter Speed
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID))
                ShutterSpeed = new Rational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID));

            //Brightness
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.BrightnessID))
                Brightness = new Rational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.BrightnessID));

            //Aperture
            if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ApertureID))
                Aperture = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ApertureID));

        }

        #endregion

        /// <summary>
        /// Get the query string used to access the passed exifID
        /// </summary>
        /// <param name="exifID">The exif ID to build the query string for</param>
        /// <returns>The query string for the BitmapMetadata queries</returns>
        private string GetQueryStringForExif(uint exifID)
        {
            return exifQueryBase + "{uint=" + exifID.ToString() + "}";
        }

        /// <summary>
        /// Get the query string used to access the passed IFD ID
        /// </summary>
        /// <param name="ID">The ID to build to the query string for</param>
        /// <returns>The query stirng for the BitmapMetadata queries</returns>
        private string GetQueryStringForIFD(uint ID)
        {
            return ifdQueryBase + "{uint=" + ID.ToString() + "}";
        }


        #region xmp data structure helpers

        /// <summary>
        /// The query string for the primary value in an xmp sequence
        /// </summary>
        private const string QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE = "/{ulong=0}";

        /// <summary>
        /// The BitmapMetadata format value for an xmp sequence
        /// </summary>
        private const string XMP_SEQUENCE_FORMAT = "xmpseq";

        /// <summary>
        /// Get the primary value from a xmp sequence
        /// </summary>
        /// <param name="metadata">The metadata of the image</param>
        /// <param name="query">The query string to the xmp sequence</param>
        /// <returns>The value of the primary item in the xmp sequence, null if it does not exist</returns>
        private object GetPrimaryValueFromXMPSequence(BitmapMetadata metadata, string query)
        {
            BitmapMetadata sequenceMetaData = (BitmapMetadata)metadata.GetQuery(query);
            if (sequenceMetaData == null)
                return null;
            return sequenceMetaData.GetQuery(QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE);
        }

        /// <summary>
        /// Set the primary value for an xmp sequence, will create the xmp sequence
        /// if it does not exist
        /// </summary>
        /// <param name="metadata">The metadata of the image</param>
        /// <param name="query">The query string to the xmp sequence</param>
        /// <param name="value">The value to save</param>
        /// <remarks>
        /// The value will be saved in its string format.
        /// Value cannot be null
        /// </remarks>
        private void SetPrimaryValueForXMPSequence(BitmapMetadata metadata, string query, object value)
        {
            Debug.Assert(value != null);
            if (metadata.ContainsQuery(query))
            {
                BitmapMetadata sequenceMetadata =
                    (BitmapMetadata)metadata.GetQuery(query);
                sequenceMetadata.SetQuery(QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE, value.ToString());
            }
            else
            {
                BitmapMetadata sequenceMetadata = new BitmapMetadata(XMP_SEQUENCE_FORMAT);
                sequenceMetadata.SetQuery(QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE, value.ToString());
                metadata.SetQuery(query, sequenceMetadata);
            }
        }


        /// <summary>
        /// The ID for getting the default element in a Lang Alt elmenent
        /// </summary>
        public const string DEFAULT_LANG_ALT_ELEMENT_QUERY = "/x-default";

        /// <summary>
        /// The BitmapMetadata format value for a xmp lang alt
        /// </summary>
        private const string XMP_LANG_ALT_FORMAT = "xmpalt";

        /// <summary>
        /// Get the primary value from a xmp lang alt 
        /// </summary>
        /// <param name="metadata">The metadata of the image</param>
        /// <param name="query">The query string to the lang alt</param>
        /// <returns>The value of the primary item in the lang alt, null if it does not exist</returns>
        private object GetPrimaryValueFromLangAlt(BitmapMetadata metadata, string query)
        {
            BitmapMetadata langAltMetaData = (BitmapMetadata)metadata.GetQuery(query);

            if (langAltMetaData == null)
                return null;

            return langAltMetaData.GetQuery(DEFAULT_LANG_ALT_ELEMENT_QUERY);
        }


        /// <summary>
        /// Set the primary value for a xmp lang alt, will create the lang alt
        /// if it does not exist
        /// </summary>
        /// <param name="metadata">The metadata of the image</param>
        /// <param name="query">The query string to the lang alt</param>
        /// <param name="value">The value to save</param>
        /// <remarks>
        /// The value will be saved in its string format.
        /// Value cannot be null
        /// </remarks>
        private void SetPrimaryValueForLangAlt(BitmapMetadata metadata, string query, object value)
        {
            Debug.Assert(value != null);

            if (metadata.ContainsQuery(query))
            {
                BitmapMetadata langAltMetadata = (BitmapMetadata)metadata.GetQuery(query);
                langAltMetadata.SetQuery(DEFAULT_LANG_ALT_ELEMENT_QUERY, value.ToString());
            }
            else
            {
                BitmapMetadata langAltMetadata = new BitmapMetadata(XMP_LANG_ALT_FORMAT);
                langAltMetadata.SetQuery(DEFAULT_LANG_ALT_ELEMENT_QUERY, value.ToString());
                metadata.SetQuery(query, langAltMetadata);
            }
        }

        #endregion
    }
}
